<?php
/**
 * This file defines the {@see \BF\Drawing\Size} class.
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\Drawing
 * @since      2015-01-31 00:29
 * @subpackage Core
 * @version    0.1
 */

namespace BF\Drawing
{

   /**
    * …
    *
    * @since      v0.1
    */
   class Size
   {

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   F I E L D S   - - - - - - - - - - - - - - - - - - - - - - -">

      /**
       * The current width.
       *
       * @var integer
       */
      public $Width;

      /**
       * The current height.
       *
       * @var integer
       */
      public $Height;

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - >   P U B L I C   C O N S T R U C T O R   - - - - - - - - - - - - - - - - - -">

      /**
       * Inits a new instance.
       *
       * @param  integer $width  The current width.
       * @param  integer $height The current height.
       */
      public function __construct( $width = 0, $height = 0 )
      {

         $this->Width  = $width;
         $this->Height = $height;

         if ( $this->Width < 0 )
         {
            $this->Width = 0;
         }

         if ( $this->Height < 0 )
         {
            $this->Height = 0;
         }

      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   M E T H O D S   - - - - - - - - - - - - - - - - - - - - - -">

      # <editor-fold defaultstate="collapsed" desc="Checking Methods">

      /**
       * Returns if one of the 2 class properties is 0 or lower.
       *
       * @return boolean
       */
      public function isEmpty()
      {
         return $this->Width <= 0 || $this->Height <= 0;
      }

      /**
       * Returns if the current size can contain the size dimentions of the defined size.
       *
       * @param \BF\Drawing\Size $size
       */
      public final function contains( \OSF\Drawing\Size $size )
      {
         return (
               ( $size->Width <= $this->Width )
            &&
               ( $size->Height <= $this->Height )
         );
      }

      /**
       * Returns if the current size defines a quadratic size (width === height)
       *
       * @return boolean
       */
      public final function isQuadratic()
      {
          return ( $this->Width == $this->Height );
      }

      /**
       * Returns if the current size uses a portrait format (width &lt; height)
       *
       * @return boolean
       */
      public final function isPortrait()
      {
          return ( $this->Width < $this->Height );
      }

      /**
       * Returns if the current size uses a portrait format (width &gt; height)
       *
       * @return boolean
       */
      public final function isLandscape()
      {
          return ( $this->Width > $this->Height );
      }

      /**
       * Returns if the current size uses a near quadratic format. It means, if width and height
       * difference is lower or equal to 15 percent.
       *
       * @param double $maxDifference If you want to change the allowed difference percent value you can do it here.
       *                              Valid values here are 0.01 (means 1%) to 0.3 (means 30%) default is 0.15
       * @return boolean
       */
      public final function isNearQuadratic( $maxDifference = 0.15 )
      {
         if ( $this->isQuadratic() )
         {
            return true;
         }
         if ( $maxDifference >= 0.01 && $maxDifference <= 0.3 )
         {
            $diff = 1.0 + $maxDifference;
         }
         else
         {
            $diff = 1.15;
         }
         if ( $this->isPortrait() )
         {
            return ( ( 0.0 + $this->Height ) / $this->Width ) <= $diff;
         }
         return ( ( 0.0 + $this->Width ) / $this->Height ) <= $diff;
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Contracting Methods">

      /**
       * Contracts both sides (width + height) by the defined value. (w=100, h=200) contracted by 50 is (w=50 h=150)
       *
       * @param integer $value
       */
      public final function contract( $value = 1 )
      {
         $this->Width = $this->Width - $value;
         $this->Height = $this->Height - $value;
      }

      /**
       * Contracts both sides (width + height) by the defined percent value. (w=100, h=200) contracted by 10%
       * is (w=90 h=180).
       *
       * @param  integer $percent The percent value for contraction. Must be lower than 100!
       * @throws \BF\ArgumentOutOfRangeException if $percent is not lower than 100 or if its lower than 1
       */
      public final function contractByPercent( $percent )
      {
         if ( $percent >= 100 || $percent < 1 )
         {
            throw new \BF\ArgumentOutOfRangeException(
               'percent',
               $percent,
               'Contraction only works with procentual values lower 100 and biger than 0'
            );
         }
         $this->Width  = \intval( \floor( $this->Width  * ( ( 100 - $percent ) / 100 ) ) );
         $this->Height = \intval( \floor( $this->Height * ( ( 100 - $percent ) / 100 ) ) );
      }

      /**
       * Contracts the current size to fit the defined Size, by holding its proportions.
       *
       * @param  \BF\Drawing\Size $maxSize
       * @return boolean TRUE on success, or FALSE, if current size is already smaller than defined size.
       */
      public final function contractToMaxSize( \BF\Drawing\Size $maxSize )
      {

         if ( ( $this->Width  < $maxSize->Width )
           && ( $this->Height < $maxSize->Height ) )
         {
            return false;
         }

         if ( ( $this->Width  === $maxSize->Width )
           && ( $this->Height === $maxSize->Height ) )
         {
            return true;
         }

         $dw = $this->Width / $maxSize->Width;
         $dh = $this->Height / $maxSize->Height;

         if ( $dw < $dh )
         {
            $this->Width = \intval(
               \floor(
                  ( $this->Width * $maxSize->Height ) / $this->Height
               )
            );
            $this->Height = $maxSize->Height;
         }
         elseif ( $dw > $dh )
         {
            $this->Height = \intval(
               \floor(
                  ( $maxSize->Width * $this->Height ) / $this->Width
               )
            );
            $this->Width = $maxSize->Width;
         }
         else # ($dw == $dh)
         {
            $this->Width  = $maxSize->Width;
            $this->Height = $maxSize->Height;
         }

         return true;

      }

      /**
       * Contracts the longest side to the defined length and also contracts the shorter side to hold the proportion
       * of this size. If the currennt size defines a quadratic size, its alos contracted but with searching the
       * longer side.
       *
       * @param  integer $newMaxSideLength
       * @return boolean TRUE on success, or FALSE if longest side is already shorter or equal to $newMaxSideLength.
       */
      public final function contractMaxSideTo( $newMaxSideLength )
      {

         if ( $this->isPortrait() )
         {

            if ( $newMaxSideLength >= $this->Height )
            {
               return false;
            }
            $resultPercent = ( 100 * $newMaxSideLength ) / $this->Height;
            $this->Width = \intval(
               \floor(
                  ( $resultPercent * $this->Width ) / 100
               )
            );
            $this->Height = $newMaxSideLength;

         }
         else
         {

            if ( $newMaxSideLength >= $this->Width )
            {
               return false;
            }
            $resultPercent = ( 100 * $newMaxSideLength ) / $this->Width;
            $this->Height = \intval(
               \floor(
                  ( $resultPercent * $this->Height ) / 100
               )
            );
            $this->Width = $newMaxSideLength;

         }

         return true;

      }

      /**
       * Contracts the longest side to the defined length and also contracts the shorter side to hold the proportion
       * of this size. The value, used as max side length is depending to the current size format.
       *
       * $newLandscapeMaxWidth is used if the current size uses a landscape ord quadratic format. Otherwise the
       * $newPortraitMaxHeight is used.
       *
       * @param  integer $newLandscapeMaxWidth The max width, used if size has landscape or quadratic format.
       * @param  integer $newPortraitMaxHeight The max height, used if size has portrait format.
       * @return boolean
       */
      public final function contractMaxSideTo2( $newLandscapeMaxWidth, $newPortraitMaxHeight )
      {

         if ( $this->isPortrait() )
         {

            if ( $newPortraitMaxHeight >= $this->Height )
            {
               return false;
            }
            $resultPercent = ( 100 * $newPortraitMaxHeight ) / $this->Height;
            $this->Width = \intval(
               \floor(
                  ( $resultPercent * $this->Width ) / 100
               )
            );
            $this->Height = $newPortraitMaxHeight;

         }
         else
         {

            if ( $newLandscapeMaxWidth >= $this->Width )
            {
               return false;
            }
            $resultPercent = ( 100 * $newLandscapeMaxWidth ) / $this->Width;
            $this->Height = \intval(
               \floor(
                  ( $resultPercent * $this->Height ) / 100
               )
            );
            $this->Width = $newLandscapeMaxWidth;

         }

         return true;

      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Expanding Methods">

      /**
       * Expands both sides (width + height) by the defined value. (w=100, h=200) expanded by 50 is (w=150 h=250)
       *
       * @param integer $value
       */
      public final function expand( $value = 1 )
      {
         $this->Width  = $this->Width + $value;
         $this->Height = $this->Height + $value;
      }

      /**
       * Expands the longest side to the defined length and also expands the shorter side to hold the proportion
       * of this size. If the currennt size defines a quadratic size, its also expanded but without searching the
       * longer side.
       *
       * @param  integer $newMaxSideLength
       * @return boolean TRUE on success, or FALSE if longest side is already longer than $newMaxSideLength.
       */
      public final function expandMaxSideTo( $newMaxSideLength )
      {

         if ( $this->isPortrait() )
         {

            if ( $newMaxSideLength < $this->Height )
            {
               return false;
            }
            $resultPercent = ( 100 * $newMaxSideLength ) / $this->Height;
            $this->Width = \intval(
               \floor(
                  ( $resultPercent * $this->Width ) / 100
               )
            );
            $this->Height = $newMaxSideLength;

         }
         else
         {

            if ( $newMaxSideLength < $this->Width )
            {
               return false;
            }
            $resultPercent = ( 100 * $newMaxSideLength ) / $this->Width;
            $this->Height = \intval(
               \floor(
                  ( $resultPercent * $this->Height ) / 100
               )
            );
            $this->Width = $newMaxSideLength;

         }

         return true;

      }

      /**
       * Expands the longest side to the defined length and also expands the shorter side to hold the proportion
       * of this size. The value, used as max side length is depending to the current size format.
       *
       * $newLandscapeMaxWidth is used if the current size uses a landscape ord quadratic format. Otherwise the
       * $newPortraitMaxHeight is used.
       *
       * @param  integer $newLandscapeMaxWidth The max width, used if size has landscape or quadratic format.
       * @param  integer $newPortraitMaxHeight The max height, used if size has portrait format.
       * @return boolean
       */
      public final function expandMaxSideTo2( $newLandscapeMaxWidth, $newPortraitMaxHeight )
      {

         if ( $this->isPortrait() )
         {

            if ( $newPortraitMaxHeight <= $this->Height )
            {
               return false;
            }
            $resultPercent = ( 100 * $newPortraitMaxHeight ) / $this->Height;
            $this->Width = \intval(
               \floor(
                  ( $resultPercent * $this->Width ) / 100
               )
            );
            $this->Height = $newPortraitMaxHeight;

         }
         else
         {

            if ( $newLandscapeMaxWidth <= $this->Width )
            {
               return false;
            }
            $resultPercent = ( 100 * $newLandscapeMaxWidth ) / $this->Width;
            $this->Height = \intval(
               \floor(
                  ( $resultPercent * $this->Height ) / 100
               )
            );
            $this->Width = $newLandscapeMaxWidth;

         }

         return true;

      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Resizing Methods">

      /**
       * Changes both sides (width + height) by the defined value. (w=100, h=200) resized by -10 is (w=90 h=190)
       *
       * @param integer $value
       */
      public final function resize( $value = 1 )
      {
          $this->Width  = $this->Width + $value;
          $this->Height = $this->Height + $value;
      }

      /**
       * Resizes the longest side to the defined length and also resizes the shorter side to hold the proportion
       * of this size. If the currennt size defines a quadratic size, its also resized but without searching the
       * longer side.
       *
       * @param  integer $newSideLength
       * @return boolean
       */
      public final function resizeMaxSideTo( $newSideLength )
      {

         if ( $this->isPortrait() )
         {

            if ( $newSideLength == $this->Height )
            {
               return false;
            }
            if ( $newSideLength < $this->Height )
            {
               return $this->contractMaxSideTo( $newSideLength );
            }
            return $this->expandMaxSideTo( $newSideLength );

         }

         if ( $newSideLength == $this->Width )
         {
            return false;
         }

         if ( $newSideLength < $this->Width )
         {
            return $this->contractMaxSideTo( $newSideLength );
         }

         return $this->expandMaxSideTo( $newSideLength );

      }

      /**
       * Resizes the longest side to the defined length and also resizes the shorter side to hold the proportion
       * of this size. The value, used as max side length is depending to the current size format.
       *
       * $newLandscapeWidth is used if the current size uses a landscape ord quadratic format. Otherwise the
       * $newPortraitHeight is used.
       *
       * @param  int $newLandscapeWidth Maximale Breite für Querformat und Quadratisch
       * @param  int $newPortraitHeight Maximale Höhe für Hochformat
       * @return bool
       */
      public final function resizeMaxSideTo2( $newLandscapeWidth, $newPortraitHeight )
      {
         if ( $this->isPortrait() )
         {
            if ( $newPortraitHeight == $this->Height )
            {
               return false;
            }
            if ( $newPortraitHeight < $this->Height )
            {
               return $this->contractMaxSideTo2( $newLandscapeWidth, $newPortraitHeight );
            }
            return $this->expandMaxSideTo2( $newLandscapeWidth, $newPortraitHeight );
         }
         if ( $newLandscapeWidth == $this->Width )
         {
            return false;
         }
         if ( $newPortraitHeight < $this->Width )
         {
            return $this->contractMaxSideTo2( $newLandscapeWidth, $newPortraitHeight );
         }
         return $this->expandMaxSideTo2( $newLandscapeWidth, $newPortraitHeight );
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="XML-Methods">

      /**
       * Write the current instance data as XML element attributes ('width' + 'height') to defined XmlWriter.
       *
       * @param \XMLWriter $w The XmlWriter.
       */
      public final function writeXmlAttributes( \XMLWriter $w )
      {
         $w->writeAttribute( 'width',  $this->Width  );
         $w->writeAttribute( 'height', $this->Height );
      }

      /**
       * Writes the current instance data as XML element to a XmlWriter.
       *
       * The resulting XML element looks like follow:
       *
       * <b>&lt;Size width="0" height="0"/&gt;</b>
       *
       * @param \XMLWriter $w           The XmlWriter.
       * @param string     $elementName The name of the resulting XML element.  (default='Size')
       *                                If no usable element name is defined, only the attributes are written!
       */
      public final function writeXml( \XMLWriter $w, $elementName = 'Size' )
      {
         $writeElement = ! empty( $elementName );
         if ( $writeElement )
         {
            $w->startElement( $elementName );
         }
         $this->writeXmlAttributes( $w );
         if ( $writeElement )
         {
            $w->endElement();
         }
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc="Rotation Methods">

      /**
       * Rotate the current size by 90°. (In other words: width and height are exchanged.)
       */
      public final function rotateSquare()
      {
         $tmp = $this->Width;
         $this->Width = $this->Height;
         $this->Height = $tmp;
      }

      # </editor-fold>

      /**
       * Returns the size string. Format is: "width=?; height=?"
       *
       * @return string
       */
      public function __toString()
      {
          return \sprintf( 'width=%d; height=%d', $this->Width, $this->Height );
      }

      /**
       * Gibt Breite und Höhe als Array zurück Zuordnung ist:
       * 0=Breite, 1=Höhe, 'width' und 'height'
       *
       * @return array Array mit Keys 0=Breite, 1=Höhe, 'width' und 'height'
       */
      public function toArray()
      {
          return array(
              0        => $this->Width,
              1        => $this->Height,
              'width'  => $this->Width,
              'height' => $this->Height
          );
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      /**
       * Parses a array to a {@see \BF\Drawing\Size} instance.
       *
       * @param array $objectData
       * @return \BF\Drawing\Size
       */
      public static function FromArray( array $objectData )
      {

         $w = 0;
         if ( isset( $objectData[ 'width' ] ) )
         {
            $w = \inval( $objectData[ 'width' ] );
         }
         elseif ( isset( $objectData[ 'Width' ] ) )
         {
            $w = \inval( $objectData['Width'] );
         }
         elseif ( isset( $objectData[ 0 ] ) )
         {
            $w = \inval( $objectData[ 0 ] );
         }

         $h = 0;
         if ( isset( $objectData[ 'height' ] ) )
         {
            $h = \inval( $objectData[ 'height' ] );
         }
         elseif ( isset( $objectData[ 'Height' ] ) )
         {
            $h = \inval( $objectData[ 'Height' ] );
         }
         elseif ( isset( $objectData[ 1 ] ) )
         {
            $h = \inval( $objectData[ 1 ] );
         }

         return new \BF\Drawing\Size( $w, $h );

      }

      public static function FromString( $objectString )
      {

         $hits = null;
         if ( \preg_match( '~^width=(\d{1,4});\s*height=(\d{1,4})$~i', $objectString, $hits ) )
         {
            return new \BF\Drawing\Size( \intval( $hits[ 1 ] ), \intval( $hits[ 2 ] ) );
         }

         $res = \BF\ArrayHelper::ParseAttributes( $objectString, true, false );

         if ( empty( $res ) || ! isset( $res[ 'width' ] ) || ! isset( $res[ 'height' ] ) )
         {
            if ( ! \preg_match( '~^[A-Za-z0-9_.:,;/!$%~*+-]+$~', $objectString ) )
            {
               return false;
            }
            if ( ! \file_exists( $value ) )
            {
               return false;
            }
            try
            {
               $tmp = \getimagesize( $value );
            }
            catch ( \Exception $ex )
            {
               $ex = null;
               return false;
            }
            return new \BF\Drawing\Size( \intval( $tmp[ 0 ] ), \intval( $tmp[ 1 ] ) );
         }

         $w = \strval( \intval( $res[ 'width' ] ) );
         $h = \strval( \intval( $res[ 'height' ] ) );

         if ( $h != $res[ 'height' ] || $w < $res[ 'width' ] )
         {
            return false;
         }

         return new \BF\Drawing\Size( \intval( $res[ 'width' ] ), \intval( $res[ 'height' ] ) );

      }

      /**
       * Liest alle Daten der aktuellen Instanz aus dem angegebenen XmlElement
       *
       * @param \SimpleXMLElement $xmlElement
       * @params bool $strict[=false] Strikt an einen vordefinierten XML-Elementname halten?
       * @throws \OSF\ArgumentFormatException Wenn Deserialisierung nicht möglich ist.
       */
      public static function FromXml( \SimpleXMLElement $xmlElement )
      {

         // Getting Width
         $w = null;
         if ( isset( $xmlElement[ 'width' ] ) )
         {
            $w = \intval( $xmlElement[ 'width' ] );
         }
         elseif ( isset( $xmlElement[ 'Width' ] ) )
         {
            $w = \intval( $xmlElement[ 'Width' ] );
         }
         elseif ( isset( $xmlElement[ 'attributes' ][ 'width' ] ) )
         {
            $w = \intval( $xmlElement[ 'attributes' ][ 'width' ] );
         }
         elseif ( isset( $xmlElement[ 'attributes' ][ 'Width' ] ) )
         {
            $w = \intval( $xmlElement[ 'attributes' ][ 'Width' ] );
         }
         elseif ( isset( $xmlElement[ '@attributes' ][ 'width' ] ) )
         {
            $w = \intval( $xmlElement[ '@attributes' ][ 'width' ] );
         }
         elseif ( isset( $xmlElement[ '@attributes' ][ 'Width' ] ) )
         {
            $w = \intval( $xmlElement[ '@attributes' ][ 'Width' ] );
         }

         // Getting Height
         $h = null;
         if ( isset( $xmlElement[ 'height' ] ) )
         {
            $h = \intval( $xmlElement[ 'height' ] );
         }
         elseif ( isset( $xmlElement[ 'Height' ] ) )
         {
            $h = \intval( $xmlElement[ 'Height' ] );
         }
         elseif ( isset( $xmlElement[ 'attributes' ][ 'height' ] ) )
         {
            $h = \intval( $xmlElement[ 'attributes' ][ 'height' ] );
         }
         elseif ( isset( $xmlElement[ 'attributes' ][ 'Height' ] ) )
         {
            $h = \intval( $xmlElement[ 'attributes' ][ 'Height' ] );
         }
         elseif ( isset( $xmlElement[ '@attributes' ][ 'height' ] ) )
         {
            $h = \intval( $xmlElement[ '@attributes' ][ 'height' ] );
         }
         elseif ( isset( $xmlElement[ '@attributes' ][ 'Height' ] ) )
         {
            $h = \intval( $xmlElement[ '@attributes' ][ 'Height' ] );
         }

         if ( \is_null( $w ) || \is_null( $y ) )
         {
            return false;
         }

         return new \BF\Drawing\Size( $w, $h );

      }

      /**
       * Extrahiert aus dem übergebenen Wert eine {@see \OSF\Drawing\Size}
       * Instanz, die im Parameter $size zurück gegeben wird, wenn die
       * Methode selbst (bool)TRUE zurück gibt.
       *
       * @param mixed $value int|double|array|string|\SimpleXMLElement|image-resource
       * @param type $size Resultierende {@see \OSF\Drawing\Size} Instanz
       * @return boolean
       */
      public static function TryParse( $value, &$size )
      {
         $size = null;
         if ( \is_int( $value ) )
         {
            $size = new \BF\Drawing\Size( $value, $value );
            return true;
         }
         if ( \is_double( $value ) )
         {
            $size = new \BF\Drawing\Size( (int)$value, (int)$value );
            return true;
         }
         if ( \is_array( $value ) )
         {
            return ( false !== ( $size = self::FromArray( $value ) ) );
         }
         if ( \is_string( $value ) )
         {
            return ( false !== ( $size = self::FromString( $value ) ) );
         }
         if ( $value instanceof \SimpleXMLElement )
         {
            return ( false !== ( $size = self::FromXml( $value ) ) );
         }
         if ( \is_resource( $value ) && \BF\str_contains( \get_resource_type( $value ), 'image', true ) )
         {
            try
            {
               $size = new \BF\Drawing\Size( \imagesx( $value ), \imagesy( $value ) );
               return true;
            }
            catch ( \Exception $ex )
            {
               $ex = null;
               return false;
            }
         }
         return false;
      }

      /**
       * Parses a value to a {@see \BF\Drawing\Size} instance. If parsing fails, it returns boolean FALSE.
       *
       * @param  mixed $value
       * @return \BF\Drawing\Size Or boolean FALSE
       */
      public static function Parse( $value )
      {
          $res = null;
          if ( ! self::TryParse( $value, $res ) )
          {
             return false;
          }
          return $res;
      }

      /**
       * @return \BF\Drawing\Size
       * @throws \BF\IO\FileNotFoundException
       * @throws \BF\IO\Exception
       */
      public static function FromImageFile( $imageFile )
      {
         if ( ! \file_exists( $imageFile ) )
         {
            throw new \BF\IO\FileNotFoundException( $imageFile );
         }
         try
         {
            $tmp = \getimagesize( $imageFile );
            return new \BF\Drawing\Size( \intval( $tmp[ 0 ] ), \intval( $tmp[ 1 ] ) );
         }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\Exception( $imageFile, $ex->getMessage() );
         }
      }

      # </editor-fold>

   }

}

